package com.dhcc.bpm.modules.auth.service.impl;

import cn.hutool.crypto.symmetric.RC4;
import com.alibaba.fastjson.JSONObject;
import com.dhcc.bpm.common.cache.CacheNames;
import com.dhcc.bpm.common.constant.CommonConstant;
import com.dhcc.bpm.common.manager.AsyncManager;
import com.dhcc.bpm.common.manager.factory.AsyncFactory;
import com.dhcc.bpm.common.utils.AES2Util;
import com.dhcc.bpm.common.utils.DateUtils;
import com.dhcc.bpm.modules.auth.granter.ITokenGranter;
import com.dhcc.bpm.modules.auth.granter.TokenGranterBuilder;
import com.dhcc.bpm.modules.auth.granter.TokenParameter;
import com.dhcc.bpm.modules.auth.service.AuthService;
import com.dhcc.bpm.modules.auth.utils.TokenUtil;
import com.dhcc.bpm.modules.system.entity.Param;
import com.dhcc.bpm.modules.system.entity.Role;
import com.dhcc.bpm.modules.system.entity.User;
import com.dhcc.bpm.modules.system.entity.UserInfo;
import com.dhcc.bpm.modules.system.service.IParamService;
import com.dhcc.bpm.modules.system.service.IRoleService;
import com.dhcc.bpm.modules.system.service.IUserService;
import com.wf.captcha.ArithmeticCaptcha;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @program: cbus-bpm-boot
 * @ClassName AuthServiceImpl
 * @author: xiafayu
 * @create: 2022-07-08 15:26:09
 * @Version 1.0.0
 **/
@Service
public class AuthServiceImpl implements AuthService {
    private static final Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IUserService userService;
    @Autowired
    private IRoleService roleService;
    @Autowired
    private IParamService paramService;

    @Value("${bpm.isUpdatePwd}")
    private Boolean isUpdatePwd;

    private ValueOperations<String, String> opsForValue;

    @PostConstruct
    public void init() {
        opsForValue = stringRedisTemplate.opsForValue();
    }

    /**
     * 用户登录次数计数  redisKey 前缀
     */
    private static final String SHIRO_LOGIN_COUNT = "shiro_login_count:";

    /**
     * 用户登录是否被锁定  redisKey 前缀
     */
    private static final String SHIRO_IS_LOCK = "shiro_is_lock:";

    /**
     * 用户登录角色  redisKey 前缀
     */
    private static final String SHIRO_ROLE_ID = "shiro_role_id:";

    /**
     * 租户号  tenantId
     */
    private static final String TENANT_ID = "tenantId";
    /**
     * 用户名
     */
    private static final String USER_NAME = "username";
    /**
     * 用户登录密码
     */
    private static final String PASSWORD = "password";
    /**
     * 用户角色
     */
    private static final String ROLE = "role";
    /**
     * 用户角色
     */
    private static final String ROLES = "roles";
    /**
     * 登录类型
     */
    private static final String GRANT_TYPE = "grant_type";
    /**
     * 登录类型
     */
    private static final String GRANTTYPE = "grantType";
    /**
     * 刷新token
     */
    private static final String REFRESHTOKEN = "refreshToken";
    /**
     * 用户类型
     */
    private static final String USERTYPE = "userType";
    /**
     * 刷新token
     */
    private static final String REFRESH_TOKEN = "refresh_token";
    /**
     * 错误码
     */
    private static final String ERROR_CODE = "error_code";
    /**
     * 锁定标识
     */
    private static final String LOCK = "LOCK";
    /**
     * 验证码
     */
    private static final String CODE = "code";
    /**
     * 错误次数
     */
    private static final int LOCK_NUM = 3;
    /**
     * 错误描述
     */
    private static final String ERROR_DESCRIPTION = "error_description";
    /**
     * token标识
     */
    private static final String BLADE_AUTH = "Blade-Auth";
    /**
     * token标识
     */
    private static final String BEARER = "bearer ";
    /**
     * 账号或密码不能为空
     */
    private static final String ACCOUNT_OR_PASSWORD_IS_NOT_EMPTY = "账号或密码不能为空！";
    /**
     * 未获得用户的角色信息
     */
    private static final String ROLE_IS_EMPTY = "未获得用户的角色信息";
    /**
     * 登录成功
     */
    private static final String LOGIN_SUCCESS = "登录成功";
    /**
     * redis存储token后缀标识
     */
    private static final String TOKEN_SUFFIX = "@token";
    /**
     * redis存储role后缀标识
     */
    private static final String ROLE_SUFFIX = "@workFlowRole";
    /**
     * 账号或密码输入错误3次，帐号10分钟内禁止登录
     */
    private static final String ERROR_THIRD_MSG = "账号或密码输入错误超过3次，帐号10分钟内禁止登录！";
    /**
     * 账号或密码错误
     */
    private static final String ACCOUNT_PASSWORD_ERROR_MSG = "用户名或密码不正确";
    /**
     * 账户登录次数日志
     */
    private static final String LOGIN_INFO_MSG = "用户:{}，登陆的次数是:{}";
    /**
     * 验证码错误
     */
    private static final String CODE_ERR_MSG = "验证码错误，请重新输入！";

    /**
     * 账户锁定描述
     */
    private static final String ACCOUNT_LOCK_ERROR_MSG = "次该用户将被锁定10分钟。";

    @Override
    public Kv getToken(JSONObject json) {

        Kv authInfo = Kv.create();

        String tenantId = json.getString(TENANT_ID) == null ? "000000" : json.getString(TENANT_ID);
        String userName = json.getString(USER_NAME);
        String password = json.getString(PASSWORD);
        String role = json.getString(ROLE);
        String grantType = json.getString(GRANT_TYPE);
        String refreshToken = json.getString(REFRESH_TOKEN);
        String code = json.getString(CODE);

        if (!verfyCode(grantType, code)) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, CODE_ERR_MSG));
            return Kv.create().set(ERROR_CODE, HttpServletResponse.SC_FORBIDDEN).set(ERROR_DESCRIPTION, CODE_ERR_MSG);
        }

        // 解密密码
        password = checkPassword(password);
        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION, ACCOUNT_OR_PASSWORD_IS_NOT_EMPTY);
        }

        log.info(LOGIN_INFO_MSG, userName, opsForValue.get(SHIRO_LOGIN_COUNT + userName));
        role = getUserRole(userName, role);
        if(StringUtils.isEmpty(role)){
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_FORBIDDEN).set(ERROR_DESCRIPTION,
                    ROLE_IS_EMPTY);
        }
        //如果这个账号登陆异常，则在登陆页面提醒。
        if (isEnableLock(userName)) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, ERROR_THIRD_MSG));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION, ERROR_THIRD_MSG);
        }
        String userType = Func.toStr(WebUtil.getRequest().getHeader(TokenUtil.USER_TYPE_HEADER_KEY),
                TokenUtil.DEFAULT_USER_TYPE);

        TokenParameter tokenParameter = new TokenParameter();
        tokenParameter.getArgs().set(TENANT_ID, tenantId).set(USER_NAME, userName).set(PASSWORD, password)
                .set(GRANTTYPE, grantType).set(REFRESHTOKEN, refreshToken).set(USERTYPE, userType);

        ITokenGranter granter = TokenGranterBuilder.getGranter(grantType);
        UserInfo userInfo = granter.grant(tokenParameter);

        if (userInfo == null || userInfo.getUser() == null) {
            //访问一次，计数一次
            opsForValue.increment(SHIRO_LOGIN_COUNT + userName, 1);  //每次增加1
            log.info(LOGIN_INFO_MSG, userName, opsForValue.get(SHIRO_LOGIN_COUNT + userName));
            //实现锁定
            return check(userName);
        }

        if (Func.isEmpty(userInfo.getRoles())) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, ROLE_IS_EMPTY));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION,
                    ROLE_IS_EMPTY);
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_SUCCESS, LOGIN_SUCCESS));
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        String passdate = df.format(userInfo.getUser().getUpdateTime());
        log.info("用户密码最后更新日期：{}", passdate);
        int deadLineDay = 0;
        try {
            deadLineDay = Integer.parseInt(DateUtils.pubDeadLineDay(passdate, 90).replace("-", ""));
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        int nowdate = Integer.parseInt(df.format(new Date()).replace("-", ""));
        log.info("现在日期：{}", nowdate);
        if (nowdate > deadLineDay && Boolean.TRUE.equals(isUpdatePwd)) {
            log.info("密码已经过期");
            userInfo.getUser().setStatus(10);//密码过期临时状态
        }
        //清空登录计数
        opsForValue.set(SHIRO_LOGIN_COUNT + userName, "0");
        //清空锁
        opsForValue.set(SHIRO_IS_LOCK + userName, "");
        List<Role> roles = roleService.getRoleSelectByIds(role);
        if (roles.isEmpty()) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, ROLE_IS_EMPTY));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_FORBIDDEN).set(ERROR_DESCRIPTION,
                    ROLE_IS_EMPTY);
        }
        opsForValue.set(SHIRO_ROLE_ID + userName, role);
        Kv auth = TokenUtil.createAuthInfo(userInfo, roles.get(0));
        String token = auth.getStr(TokenConstant.ACCESS_TOKEN);
        //登录token存到redis, 有效期默认60分钟
        opsForValue.set(auth.getStr(TokenConstant.ACCOUNT) + TOKEN_SUFFIX, token, 3600, TimeUnit.SECONDS);
        //删除上一个账号审批菜单权限
        return auth;
    }

    @Override
    public Kv getSsoToken(String role, String userName) {
        Kv authInfo = Kv.create();
        User user = userService.selectUserByaccount(userName);
        UserInfo userInfo = userService.userInfo(user.getId());
        if (Func.isEmpty(userInfo.getRoles())) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, ROLE_IS_EMPTY));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION,
                    ROLE_IS_EMPTY);
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_SUCCESS, LOGIN_SUCCESS));
        List<Role> roles = roleService.getRoleSelectByIds(role);
        if (roles.isEmpty()) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, ROLE_IS_EMPTY));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_FORBIDDEN).set(ERROR_DESCRIPTION,
                    ROLE_IS_EMPTY);
        }
        opsForValue.set(SHIRO_ROLE_ID + userName, role);
        Kv auth = TokenUtil.createAuthInfo(userInfo, roles.get(0));
        String token = auth.getStr(TokenConstant.ACCESS_TOKEN);
        //登录token存到redis, 有效期默认60分钟
        opsForValue.set(auth.getStr(TokenConstant.ACCOUNT) + TOKEN_SUFFIX, token, 3600, TimeUnit.SECONDS);
        return auth;
    }

    @Override
    public Kv getRole(JSONObject json) {

        Kv authInfo = Kv.create();

        String tenantId = json.getString(TENANT_ID) == null ? "000000" : json.getString(TENANT_ID);
        String userName = json.getString(USER_NAME);
        String password = json.getString(PASSWORD);
        String grantType = json.getString(GRANT_TYPE);
        String refreshToken = json.getString(REFRESH_TOKEN);
        // 解密密码
        try {
            Param param = paramService.getParamByKey(CommonConstant.PASS_SECRET);
            String decryptKey = param == null ? CommonConstant.INIT_SECRET : param.getParamValue();

            password = AES2Util.decrypt(password, decryptKey);
        } catch (Exception e) {
            log.error("解密密码异常:", e);
        }
        if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION, ACCOUNT_OR_PASSWORD_IS_NOT_EMPTY);
        }

        log.info(LOGIN_INFO_MSG, userName, opsForValue.get(SHIRO_LOGIN_COUNT + userName));

        //如果这个账号登陆异常，则在登陆页面提醒。
        //计数大于3次，设置用户被锁定一分钟
        if (isEnableLock(userName)) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, ERROR_THIRD_MSG));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION, ERROR_THIRD_MSG);
        }
        String userType = Func.toStr(WebUtil.getRequest().getHeader(TokenUtil.USER_TYPE_HEADER_KEY),
                TokenUtil.DEFAULT_USER_TYPE);

        TokenParameter tokenParameter = new TokenParameter();
        tokenParameter.getArgs().set(TENANT_ID, tenantId).set(USER_NAME, userName).set(PASSWORD, password)
                .set(GRANTTYPE, grantType).set(REFRESHTOKEN, refreshToken).set(USERTYPE, userType);

        ITokenGranter granter = TokenGranterBuilder.getGranter(grantType);
        UserInfo userInfo = granter.grant(tokenParameter);

        if (userInfo == null || userInfo.getUser() == null) {
            //访问一次，计数一次
            opsForValue.increment(SHIRO_LOGIN_COUNT + userName, 1);  //每次增加1
            log.info(LOGIN_INFO_MSG, userName, opsForValue.get(SHIRO_LOGIN_COUNT + userName));
            //实现锁定
            return check(userName);
        }

        if (Func.isEmpty(userInfo.getRoles())) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_FAIL, ROLE_IS_EMPTY));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION,
                    ROLE_IS_EMPTY);
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, CommonConstant.LOGIN_SUCCESS, LOGIN_SUCCESS));
        //清空登录计数
        opsForValue.set(SHIRO_LOGIN_COUNT + userName, "0");
        //清空锁
        opsForValue.set(SHIRO_IS_LOCK + userName, "");
        Kv role = Kv.create();
        List<Role> roles = roleService.getRoleSelectByIds(userInfo.getUser().getRoleId());
        role.set(ROLES, roles);
        return role;
    }

    @Override
    public Kv createToken(HttpServletRequest request) {
        UserInfo userInfo = new UserInfo();
        User user = new User();
        BladeUser bladeUser = AuthUtil.getUser(request);
        BeanUtils.copyProperties(bladeUser, user);
        userInfo.setUser(user);
        List<Role> roles = roleService.getRoleSelectByIds(bladeUser.getRoleId());
        return TokenUtil.createAuthInfo(userInfo, roles.get(0));
    }

    @Override
    public Kv getSsoRole(HttpServletRequest request) {
        String ssoUserName = request.getHeader(USER_NAME);
        String username = ssoUserName;
        Kv authInfo = Kv.create();
        User user = userService.selectUserByaccount(username);
        UserInfo userInfo = userService.userInfo(user.getId());
        if (Func.isEmpty(userInfo.getRoles())) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, CommonConstant.LOGIN_FAIL, ROLE_IS_EMPTY));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION,
                    ROLE_IS_EMPTY);
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, CommonConstant.LOGIN_SUCCESS, LOGIN_SUCCESS));
        Kv role = Kv.create();
        List<Role> roles = roleService.getRoleSelectByIds(userInfo.getUser().getRoleId());
        role.set(ROLES, roles);
        return role;
    }

    @Override
    public boolean logout(HttpServletRequest request) {
        BladeUser bladeUser = AuthUtil.getUser(request);
        if (bladeUser != null) {
            //redis存储的token
            String authToken = opsForValue.get(bladeUser.getAccount() + TOKEN_SUFFIX);
            //请求token
            String token = request.getHeader(BLADE_AUTH);
            if (token == null) {
                token = request.getParameter(BLADE_AUTH);
            }
            //判断是否为后登录用户退出登录，后登录用户退出登录须删除token，先登录用户token被覆盖无须删除
            if (token.replace(BEARER, "").equals(authToken)) {
                stringRedisTemplate.delete(bladeUser.getAccount() + TOKEN_SUFFIX);//根据key删除缓存
                //工作流校验用户是否拥有流程设计权限
                stringRedisTemplate.delete(bladeUser.getAccount() + ROLE_SUFFIX);
            }
        }
        return true;
    }

    @Override
    public Kv createCaptcha() {
        // 算术类型
        ArithmeticCaptcha captcha = new ArithmeticCaptcha(110, 48);
        captcha.setLen(3);  // 几位数运算，默认是两位
        captcha.getArithmeticString();  // 获取运算的公式：3+2=?
        captcha.text();  // 获取运算的结果：5

        String verCode = captcha.text().toLowerCase();
        String key = UUID.randomUUID().toString();
        // 存入redis并设置过期时间为30分钟
        stringRedisTemplate.opsForValue().set(CacheNames.CAPTCHA_KEY + key, verCode, 5L, TimeUnit.MINUTES);
        // 将key和base64返回给前端
        return Kv.create().set("key", key).set("image", captcha.toBase64());
    }

    @Override
    public String getAuthCode() {
        RC4 rc4 = new RC4(cn.hutool.core.lang.UUID.randomUUID().toString());
        String authCode = rc4.encryptHex(String.format("%08d", 1).getBytes(StandardCharsets.UTF_8));
        //授权码有效时间1分钟
        stringRedisTemplate.opsForValue().set("authCode:" + authCode, authCode, 1, TimeUnit.MINUTES);
        return authCode;
    }

    @Override
    public boolean verifyAuthCode(JSONObject json) {
        String authCode = json.getString("authCode");
        String code = stringRedisTemplate.opsForValue().get("authCode:" + authCode);
        if (StringUtils.isBlank(code) || StringUtils.isBlank(authCode)) {
            return false;
        }
        return authCode.equals(code);
    }


    private Kv check(String username) {
        Kv authInfo = Kv.create();
        String times = opsForValue.get(SHIRO_LOGIN_COUNT + username);
        if (!org.apache.commons.lang.StringUtils.isBlank(times) && Integer.parseInt(times) >= LOCK_NUM) {
            //锁住这个账号，值是LOCK。
            opsForValue.set(SHIRO_IS_LOCK + username, LOCK);
            //expire  变量存活期限
            stringRedisTemplate.expire(SHIRO_IS_LOCK + username, 10, TimeUnit.MINUTES);
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, CommonConstant.LOGIN_FAIL, ERROR_THIRD_MSG));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION, ERROR_THIRD_MSG);
        } else {
            int num = LOCK_NUM - (org.apache.commons.lang.StringUtils.isBlank(times) ? 0 : Integer.parseInt(times));
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, CommonConstant.LOGIN_FAIL, ACCOUNT_PASSWORD_ERROR_MSG));
            return authInfo.set(ERROR_CODE, HttpServletResponse.SC_BAD_REQUEST).set(ERROR_DESCRIPTION, ACCOUNT_PASSWORD_ERROR_MSG + num + ACCOUNT_LOCK_ERROR_MSG);
        }
    }

    /**
     * 校验用户密码
     *
     * @param password
     * @return
     */
    private String checkPassword(String password) {
        // 解密密码
        try {
            Param param = paramService.getParamByKey(CommonConstant.PASS_SECRET);
            String decryptKey = param == null ? CommonConstant.INIT_SECRET : param.getParamValue();
            password = AES2Util.decrypt(password, decryptKey);
        } catch (Exception e) {
            log.error("解密密码异常:", e);
        }

        return password;
    }

    /**
     * 获取用户登录角色
     *
     * @param userName
     * @param role
     * @return
     */
    private String getUserRole(String userName, String role) {
        log.info(LOGIN_INFO_MSG, userName, opsForValue.get(SHIRO_LOGIN_COUNT + userName));
        if (org.apache.commons.lang.StringUtils.isEmpty(role)) {
            return opsForValue.get(SHIRO_ROLE_ID + userName);
        }
        return role;
    }


    public boolean verfyCode(String grantType, String code) {
        //管理端登录增加验证码
        if (TokenUtil.GRANT_TYPE.equals(grantType)) {
            HttpServletRequest request = WebUtil.getRequest();
            String key = request.getHeader(TokenUtil.CAPTCHA_HEADER_KEY);
            // 获取验证码
            String redisCode = String.valueOf(opsForValue.get(CacheNames.CAPTCHA_KEY + key));
            stringRedisTemplate.delete(CacheNames.CAPTCHA_KEY + key);
            // 判断验证码
            if (StringUtils.isEmpty(code) || !StringUtil.equalsIgnoreCase(redisCode, code)) {
                return false;
            }
        }
        return true;
    }

    public boolean isEnableLock(String userName) {
        if (opsForValue.get(SHIRO_LOGIN_COUNT + userName) == null) {
            return false;
        }
        if (Integer.parseInt(opsForValue.get(SHIRO_LOGIN_COUNT + userName)) >= LOCK_NUM) {
            if (LOCK.equals(opsForValue.get(SHIRO_IS_LOCK + userName))) {
                return true;
            } else {
                //清空登录计数
                opsForValue.set(SHIRO_LOGIN_COUNT + userName, "0");
            }
        }
        return false;
    }
}
